package io;

import static util.Constant.TAG_ID;
import static util.Constant.PATH_DATA;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

import util.Tool;

/**
 * The InOutput {@code class} containing all necessary functions to write XML.
 * 
 * @author Peter NGO, Guillaume POUSSEL and Mathieu RUL
 * @version 1.0
 */
public class InOutput {

    /**
     * Write an empty file, with the root tag given to the file given.
     * 
     * @param root The tag root of the XML document
     * @param file Output file
     */
    public static void writeEmpty(String root, String file) {
        new File(PATH_DATA).mkdirs();
        Document document = new Document(new Element(root));
        write(document, new File(file));
    }

    /**
     * Write the file given as an empty XML file.
     * 
     * @param file Output file
     */
    public static void writeEmpty(String file) {
        writeEmpty(
                new File(file).getName().substring(0, new File(file).getName().lastIndexOf('.')),
                file);
    }

    /**
     * Write an HashTable of Object
     * 
     * @param hashTable The HashTable with an Integer key and an object
     * @return DOM document crated
     */
    @SuppressWarnings("unchecked")
    public static Document write(Hashtable<Integer, Object> hashTable, String file) {
        if (hashTable.isEmpty()) {
            writeEmpty(file);
            return null;
        }
        // Get the class name
        Class<?> classObject = hashTable.values().iterator().next().getClass();
        boolean isItem = classObject.getGenericSuperclass().toString().equals(
                "class base.items.Item");
        String strClass = isItem ? "item" : classObject.getSimpleName().toLowerCase();
        Pattern pattern = Pattern.compile("get*");
        Method[] methods = null;
        if (!isItem) {
            methods = classObject.getMethods();
            for (int i = 0; i < methods.length; i++) {
                Matcher matcher = pattern.matcher(methods[i].getName());
                if (!matcher.lookingAt()) {
                    methods[i] = null;
                }
            }
        }
        // Create the XML
        Element root = new Element(Tool.plural(isItem ? "item" : classObject.getSimpleName())
                .toLowerCase());

        // Parcourir les valeurs de l'HashTable
        for (Object object : hashTable.values()) {
            if (object == null)
                continue;
            if (isItem) {
                // Si c'est un Item, on doit rechercher ses méthodes
                classObject = object.getClass();
                methods = classObject.getMethods();
                for (int k = 0; k < methods.length; k++) {
                    Matcher matcher = pattern.matcher(methods[k].getName());
                    if (!matcher.lookingAt()) {
                        methods[k] = null;
                    }
                }
            }
            Element nodeObject = new Element(strClass);
            for (int j = 0; j < methods.length; j++) {
                if (null != methods[j] && !methods[j].getName().equals("getClass")) {
                    Object newObject;
                    try {
                        newObject = methods[j].invoke(object);
                        if (newObject == null)
                            continue;
                        String var = methods[j].getName();
                        var = var.substring(3, var.length());
                        if (var.equals(TAG_ID.toUpperCase())) {
                            // Add object ID
                            nodeObject.setAttribute(TAG_ID, newObject.toString());
                        } else {
                            // Get type of member
                            String type = methods[j].getReturnType().getSimpleName();
                            if (type.equals("HashSet")) {
                                Element nodeSubObject = new Element(Tool.toVarName(var));
                                HashSet<Object> hashSet = (HashSet<Object>) newObject;

                                for (Object o : hashSet) {
                                    // Add sub object info
                                    Element subObject = new Element(var.substring(0,
                                            var.length() - 1).toLowerCase());
                                    subObject.setAttribute(TAG_ID, o.toString());
                                    nodeSubObject.addContent(subObject);
                                }
                                nodeObject.addContent(nodeSubObject);
                            } else if (type.equals("GregorianCalendar")) {
                                nodeObject.addContent(date2Xml((GregorianCalendar) newObject, Tool
                                        .toVarName(var)));
                            } else if (var.equals("Type")) {
                                nodeObject.setAttribute("type", newObject.toString());
                            } else {
                                // Add object info
                                nodeObject.addContent(new Element(Tool.toVarName(var))
                                        .addContent(newObject.toString()));
                            }
                        }
                    } catch (IllegalArgumentException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
            root.addContent(nodeObject);
        }
        Document document = new Document(root);
        write(document, new File(file));
        return document;
    }

    /**
     * Ecrit un document XML dans le fichier passé en paramètre.
     * 
     * Cette méthode reçoit les exceptions et affiche une erreur si l'écriture
     * du fichier échoue.
     * 
     * @param document Le document XML généré par JDOM
     * @param file Le fichier où l'on veut écrire ce document XML
     */
    private static void write(Document document, File file) {
        try {
            XMLOutputter sortie = new XMLOutputter(Format.getPrettyFormat());
            sortie.output(document, new FileOutputStream(file));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Write an HashTable of Object
     * 
     * @param file The file we want to read
     * @return The Hastable
     */
    @SuppressWarnings("unchecked")
    public static Hashtable<Integer, Object> read(String file) {
        Document document = null;
        if (!new File(file).isFile())
            writeEmpty(file);
        try {
            document = new SAXBuilder().build(file);
        } catch (JDOMException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        Element root = document.getRootElement();
        String strClass = Tool.toClassName(Tool.singular(root.getName()));
        
        boolean isItem = false;
        try {
            Class.forName("base.items."+strClass);
            isItem = true;
        } catch (ClassNotFoundException e) { }
        
        strClass = "base." + (isItem ? "items." : "") + strClass;
        Hashtable<Integer, Object> hashtable = new Hashtable<Integer, Object>();

        for (Object objetBalise : root.getChildren(Tool.singular(root.getName()))) {
            Integer id;
            try {
                Element balise = ((Element) objetBalise);
                Class classe = isItem ? Class.forName("base.items."
                        + balise.getAttributeValue("type")) : Class.forName(strClass);
                Constructor<?> constructeur;
                try {
                    constructeur = classe.getConstructor(int.class);
                } catch (NoSuchMethodException e) {
                    constructeur = classe.getConstructor(Integer.class);
                }

                id = Integer.valueOf(balise.getAttribute(TAG_ID).getValue());
                Object objet = constructeur.newInstance(id);

                Method[] methodes = classe.getMethods();
                Pattern pattern = Pattern.compile("set*");
                for (int i = 0; i < methodes.length; i++) {
                    String methodName = methodes[i].getName();
                    String varName = Tool.toVarName(methodName.substring(3, methodName.length()));
                    Matcher matcher = pattern.matcher(methodName);

                    if (matcher.lookingAt()) {
                        Class<?> type = methodes[i].getParameterTypes()[0];
                        if (methodName.equals("setType")) {
                            methodes[i].invoke(objet, balise.getAttributeValue("type"));
                            continue;
                        }
                        if (balise.getChild(varName) == null)
                            continue;

                        if (type.getSimpleName().equals("GregorianCalendar")) {
                            methodes[i].invoke(objet, xml2Date(balise.getChild(varName)));
                        } else if (type.getSimpleName().equals("int")
                                || type.getSimpleName().equals("Integer")) {
                            methodes[i]
                                    .invoke(objet, Integer.valueOf(balise.getChildText(varName)));
                        } else if (type.getSimpleName().equals("boolean")
                                || type.getSimpleName().equals("Boolean")) {
                            methodes[i]
                                    .invoke(objet, Boolean.valueOf(balise.getChildText(varName)));
                        } else if (type.getSimpleName().equals("double")
                                || type.getSimpleName().equals("Double")) {
                            methodes[i].invoke(objet, Double.valueOf(balise.getChildText(varName)));
                        } else if (type.getSimpleName().equals("float")
                                || type.getSimpleName().equals("Float")) {
                            methodes[i].invoke(objet, Float.valueOf(balise.getChildText(varName)));
                        } else if (type.getSimpleName().equals("HashSet")) {
                            HashSet<Integer> set = new HashSet<Integer>();
                            for (Object fils : balise.getChild(varName).getChildren()) {
                                set
                                        .add(Integer.valueOf(((Element) fils)
                                                .getAttributeValue(TAG_ID)));
                            }
                            methodes[i].invoke(objet, set);
                        } else {
                            methodes[i].invoke(objet, type.cast(balise.getChildText(varName)));
                        }

                        hashtable.put(id, objet);
                    }
                }
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (SecurityException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InstantiationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return hashtable;
    }

    /**
     * Cette méthode permet de lire une date à partir d'une élément de l'abre
     * DOM
     * 
     * @param dateElement La balise "date" du document XML
     * @return Une date Java
     */
    private static GregorianCalendar xml2Date(Element dateElement) {
        return new GregorianCalendar(Integer.valueOf(dateElement.getChildTextNormalize("year")),
                Integer.valueOf(dateElement.getChildTextNormalize("month")), Integer
                        .valueOf(dateElement.getChildTextNormalize("day")), Integer
                        .valueOf(dateElement.getChildTextNormalize("hour")), Integer
                        .valueOf(dateElement.getChildTextNormalize("minute")), Integer
                        .valueOf(dateElement.getChildTextNormalize("second")));
    }

    /**
     * Cette méthode permet d'écrire une date au format XML
     * 
     * @param date La date Java
     * @return Un élément XML (balise "date")
     */
    private static Element date2Xml(GregorianCalendar date, String baliseName) {
        Element dateBalise = new Element(baliseName);

        dateBalise.addContent(new Element("year").addContent(String
                .valueOf(date.get(Calendar.YEAR))));
        dateBalise.addContent(new Element("month").addContent(String.valueOf(date
                .get(Calendar.MONTH))));
        dateBalise.addContent(new Element("day").addContent(String.valueOf(date
                .get(Calendar.DAY_OF_WEEK_IN_MONTH))));
        dateBalise.addContent(new Element("hour").addContent(String
                .valueOf(date.get(Calendar.HOUR))));
        dateBalise.addContent(new Element("minute").addContent(String.valueOf(date
                .get(Calendar.MINUTE))));
        dateBalise.addContent(new Element("second").addContent(String.valueOf(date
                .get(Calendar.SECOND))));
        return dateBalise;
    }
}